import { AnimationMixer3D, AnimationUtils3D, Clock, Color, Fog3D, MeshPhongMaterial3D, Node3D, PerspectiveCamera3D, PlaneGeometry3D, Scene3D, SkeletonHelper3D, StatsComponent, WebGLRenderer3D } from 'feng3d-three';
import GUI from 'lil-gui';
import { GLTFLoader3D } from '../../extension/loaders/GLTFLoader3D';

import { IEvent } from '@feng3d/event';
import { $set } from '@feng3d/serialization';
import { AnimationAction3D } from 'feng3d-three';
import '../../extension/controls/OrbitControls3D';

const gltfPath = '../../models/gltf/Xbot.glb';

const root = new Node3D();

let scene: Scene3D;
let renderer: WebGLRenderer3D;
let camera: PerspectiveCamera3D;
let stats: StatsComponent;
let model: Node3D;
let skeleton: SkeletonHelper3D;
let mixer: AnimationMixer3D;
let clock: Clock;

const crossFadeControls = [];

let currentBaseAction = 'idle';
const allActions: AnimationAction3D[] = [];
const baseActions = {
    idle: { weight: 1 },
    walk: { weight: 0 },
    run: { weight: 0 }
};
const additiveActions = {
    sneak_pose: { weight: 0 },
    sad_pose: { weight: 0 },
    agree: { weight: 0 },
    headShake: { weight: 0 }
};
let panelSettings;
let numAnimations: number;

init();

function init()
{
    const container = document.getElementById('container');
    clock = new Clock();

    scene = root.addComponent('Scene3D');
    scene.background = new Color(0xa0a0a0);
    scene.fog = new Fog3D(0xa0a0a0, 10, 50);

    const hemiLight = new Node3D().addComponent('HemisphereLight3D', { color: { value: 0xffffff }, groundColor: { value: 0x444444 } });
    hemiLight.entity.position.set(0, 20, 0);
    scene.entity.addChild(hemiLight.entity);

    const dirLight = new Node3D().addComponent('DirectionalLight3D', { color: { value: 0xffffff } });
    dirLight.entity.position.set(3, 10, 10);
    dirLight.castShadow = true;
    dirLight.shadow.camera.top = 2;
    dirLight.shadow.camera.bottom = -2;
    dirLight.shadow.camera.left = -2;
    dirLight.shadow.camera.right = 2;
    dirLight.shadow.camera.near = 0.1;
    dirLight.shadow.camera.far = 40;
    scene.entity.addChild(dirLight.entity);

    // ground

    const mesh = new Node3D().addComponent('Mesh3D', {
        geometry: $set(new PlaneGeometry3D(), { width: 100, height: 100 }),
        material: $set(new MeshPhongMaterial3D(), { color: { value: 0x999999 }, depthWrite: false })
    });
    mesh.entity.rotation.x = -Math.PI / 2;
    mesh.receiveShadow = true;
    scene.entity.addChild(mesh.entity);

    const loader = new GLTFLoader3D();
    loader.load(gltfPath, function (gltf)
    {
        model = gltf.scene;
        scene.entity.addChild(model);

        model._group.traverse(function (object: any)
        {
            if (object.isMesh) object.castShadow = true;
        });

        skeleton = new Node3D().addComponent('SkeletonHelper3D', { node3D: model });
        skeleton.entity.visible = false;
        scene.entity.addChild(skeleton.entity);

        const animations = gltf.animations;
        mixer = new AnimationMixer3D(model);

        numAnimations = animations.length;

        for (let i = 0; i !== numAnimations; ++i)
        {
            let clip = animations[i];
            const name = clip.name;

            if (baseActions[name])
            {
                const action = mixer.clipAction(clip);
                activateAction(action);
                baseActions[name].action = action;
                allActions.push(action);
            }
            else if (additiveActions[name])
            {
                // Make the clip additive and remove the reference frame

                AnimationUtils3D.makeClipAdditive(clip);

                if (clip.name.endsWith('_pose'))
                {
                    clip = AnimationUtils3D.subclip(clip, clip.name, 2, 3, 30);
                }

                const action = mixer.clipAction(clip);
                activateAction(action);
                additiveActions[name].action = action;
                allActions.push(action);
            }
        }

        createPanel();

        animate();
    });

    renderer = root.addComponent('WebGLRenderer3D', {
        parameters: { antialias: true }
    });
    renderer.setPixelRatio(window.devicePixelRatio);
    renderer.setSize(window.innerWidth, window.innerHeight);
    renderer.outputEncoding = 'sRGBEncoding';
    renderer.shadowMap.enabled = true;
    container.appendChild(renderer.domElement);

    // camera
    camera = new Node3D().addComponent('PerspectiveCamera3D', { fov: 45, aspect: window.innerWidth / window.innerHeight, near: 1, far: 100 });
    camera.entity.position.set(-1, 2, 3);

    const controls = camera.addComponent('OrbitControls3D', { camera, domElement: renderer.domElement });
    controls.enablePan = false;
    controls.enableZoom = false;
    controls.target.set(0, 1, 0);
    controls.update();

    stats = root.addComponent('StatsComponent');
    container.appendChild(stats.dom);

    window.addEventListener('resize', onWindowResize);
}

function createPanel()
{
    const panel = new GUI({ width: 310 });

    const folder1 = panel.addFolder('Base Actions');
    const folder2 = panel.addFolder('Additive Action Weights');
    const folder3 = panel.addFolder('General Speed');

    panelSettings = {
        'modify time scale': 1.0
    };

    const baseNames = ['None', ...Object.keys(baseActions)];

    for (let i = 0, l = baseNames.length; i !== l; ++i)
    {
        const name = baseNames[i];
        const settings = baseActions[name];
        // eslint-disable-next-line no-loop-func
        panelSettings[name] = function ()
        {
            const currentSettings = baseActions[currentBaseAction];
            const currentAction = currentSettings ? currentSettings.action : null;
            const action = settings ? settings.action : null;

            if (currentAction !== action)
            {
                prepareCrossFade(currentAction, action, 0.35);
            }
        };

        crossFadeControls.push(folder1.add(panelSettings, name));
    }

    for (const name of Object.keys(additiveActions))
    {
        const settings = additiveActions[name];

        panelSettings[name] = settings.weight;
        folder2.add(panelSettings, name, 0.0, 1.0, 0.01).listen().onChange(function (weight)
        {
            setWeight(settings.action, weight);
            settings.weight = weight;
        });
    }

    folder3.add(panelSettings, 'modify time scale', 0.0, 1.5, 0.01).onChange(modifyTimeScale);

    folder1.open();
    folder2.open();
    folder3.open();

    crossFadeControls.forEach(function (control)
    {
        control.setInactive = function ()
        {
            control.domElement.classList.add('control-inactive');
        };

        control.setActive = function ()
        {
            control.domElement.classList.remove('control-inactive');
        };

        const settings = baseActions[control.property];

        if (!settings || !settings.weight)
        {
            control.setInactive();
        }
    });
}

function activateAction(action: AnimationAction3D)
{
    const clip = action.getClip();
    const settings = baseActions[clip.name] || additiveActions[clip.name];
    setWeight(action, settings.weight);
    action.play();
}

function modifyTimeScale(speed)
{
    mixer.timeScale = speed;
}

function prepareCrossFade(startAction: AnimationAction3D, endAction: AnimationAction3D, duration: number)
{
    // If the current action is 'idle', execute the crossfade immediately;
    // else wait until the current action has finished its current loop

    if (currentBaseAction === 'idle' || !startAction || !endAction)
    {
        executeCrossFade(startAction, endAction, duration);
    }
    else
    {
        synchronizeCrossFade(startAction, endAction, duration);
    }

    // Update control colors

    if (endAction)
    {
        const clip = endAction.getClip();
        currentBaseAction = clip.name;
    }
    else
    {
        currentBaseAction = 'None';
    }

    crossFadeControls.forEach(function (control)
    {
        const name = control.property;

        if (name === currentBaseAction)
        {
            control.setActive();
        }
        else
        {
            control.setInactive();
        }
    });
}

function synchronizeCrossFade(startAction: AnimationAction3D, endAction: AnimationAction3D, duration: number)
{
    mixer.on('loop', onLoopFinished);

    function onLoopFinished(event: IEvent<{
        target: AnimationMixer3D;
        action: AnimationAction3D;
        loopDelta: number;
    }>)
    {
        if (event.data.action === startAction)
        {
            mixer.off('loop', onLoopFinished);

            executeCrossFade(startAction, endAction, duration);
        }
    }
}

function executeCrossFade(startAction: AnimationAction3D, endAction: AnimationAction3D, duration: number)
{
    // Not only the start action, but also the end action must get a weight of 1 before fading
    // (concerning the start action this is already guaranteed in this place)

    if (endAction)
    {
        setWeight(endAction, 1);
        endAction.time = 0;

        if (startAction)
        {
            // Crossfade with warping

            startAction.crossFadeTo(endAction, duration, true);
        }
        else
        {
            // Fade in

            endAction.fadeIn(duration);
        }
    }
    else
    {
        // Fade out

        startAction.fadeOut(duration);
    }
}

// This function is needed, since animationAction.crossFadeTo() disables its start action and sets
// the start action's timeScale to ((start animation's duration) / (end animation's duration))

function setWeight(action: AnimationAction3D, weight: number)
{
    action.enabled = true;
    action.setEffectiveTimeScale(1);
    action.setEffectiveWeight(weight);
}

function onWindowResize()
{
    camera.aspect = window.innerWidth / window.innerHeight;
    camera.updateProjectionMatrix();

    renderer.setSize(window.innerWidth, window.innerHeight);
}

function animate()
{
    // Render loop

    requestAnimationFrame(animate);

    for (let i = 0; i !== numAnimations; ++i)
    {
        const action = allActions[i];
        const clip = action.getClip();
        const settings = baseActions[clip.name] || additiveActions[clip.name];
        settings.weight = action.getEffectiveWeight();
    }

    // Get the time elapsed since the last frame, used for mixer update

    const mixerUpdateDelta = clock.getDelta();

    // Update the animation mixer, the stats panel, and render this frame

    mixer.update(mixerUpdateDelta);

    stats.update();

    renderer.render(scene, camera);
}
